package dbMysql

import (
	"fmt"
	"gitee.com/kingzyt/common/log"
	. "gitee.com/kingzyt/common/util"
	"strconv"
	"strings"
	"sync"
	"time"
)

/*
template mark: $date$
*/

type ShardingByDateMode int

const (
	ShardingByDateMode_Day = 1 + iota
	ShardingByDateMode_Month
	ShardingByDateMode_Year
)

const g_tablename_date_mask = "$date$"

/*
[注意] 因为ObjPoolStat使用了这里, 所以这里不能用ObjPool, 以免互相嵌套, 无限写objPool的statLog
*/
type ShardingByDate struct {
	name string
	mode ShardingByDateMode

	mutex   sync.RWMutex
	sqlRWer *SimpleMySqlRWer

	dbTablesAllOk bool

	nextShardingDate int64
	dateMarkInSql    string
	dateMarkInt      int

	queries map[int32]*ShardingQuery // <id, shardingQuery>

	OnTableCreateOrCheckExisted func(curDateMarkInSql string, curDateMarkInt int)
}

func NewShardingByDate(name string, sqlRWer *SimpleMySqlRWer, mode ShardingByDateMode) *ShardingByDate {
	return &ShardingByDate{
		name:    name,
		mode:    mode,
		sqlRWer: sqlRWer,
		queries: map[int32]*ShardingQuery{},
	}
}

func (self *ShardingByDate) GetSqlRW() *SimpleMySqlRWer {
	return self.sqlRWer
}

func (self *ShardingByDate) GetTableDateMark() string {
	switch self.mode {
	case ShardingByDateMode_Day:
		return "d"
	case ShardingByDateMode_Month:
		return "m"
	case ShardingByDateMode_Year:
		return "y"
	default:
		panic(fmt.Errorf("sharding mode %d is not supported", self.mode))
	}
}

func (self *ShardingByDate) updateNextShardingDate() {
	year, month, day := time.Now().Date()
	switch self.mode {
	case ShardingByDateMode_Day:
		self.nextShardingDate = GetNextDayZeroClockTimestamp()
		self.dateMarkInt = year*10000 + int(month)*100 + day
	case ShardingByDateMode_Month:
		self.nextShardingDate = GetNextMonthZeroClockTimestamp()
		self.dateMarkInt = year*100 + int(month)
	case ShardingByDateMode_Year:
		self.nextShardingDate = GetNextYearZeroClockTimestamp()
		self.dateMarkInt = year
	default:
		panic(fmt.Errorf("sharding mode %d is not supported", self.mode))
	}
	self.dateMarkInSql = self.GetTableDateMark() + strconv.Itoa(self.dateMarkInt)
}

// if self.dbTablesAllOk is set to false on db failure, next Query calls will be quit (and this func will not be call) until db link fix
func (self *ShardingByDate) switchShardingTables(fixOnErr bool) bool {
	sqls := []*SqlQuery{}
	for _, q := range self.queries {
		q.queryCurrent = strings.Replace(q.queryTemplate, g_tablename_date_mask, self.dateMarkInSql, -1)
		sqls = append(sqls, &SqlQuery{
			Query: strings.Replace(q.createTableTemplate, g_tablename_date_mask, self.dateMarkInSql, -1),
			Args:  []interface{}{},
		})
	}

	sqlSwitchTable := func() {
		err := self.sqlRWer.WriteQueries(sqls)
		if err != nil {
			log.Error(self.name, "switchShardingTables err: %s", err)
		}
		self.dbTablesAllOk = err == nil
		if self.dbTablesAllOk && self.OnTableCreateOrCheckExisted != nil {
			self.OnTableCreateOrCheckExisted(self.dateMarkInSql, self.dateMarkInt)
		}
	}

	sqlSwitchTable()

	if !self.dbTablesAllOk && fixOnErr {
		go UpdateProc(time.Second*10, false, func() (unend bool) {
			self.mutex.Lock()
			defer self.mutex.Unlock()
			sqlSwitchTable()
			return !self.dbTablesAllOk
		})
	}
	return self.dbTablesAllOk
}

func (self *ShardingByDate) AddQueryDef(id int32, args ...interface{}) {
	if len(args) != 2 {
		panic(fmt.Errorf("%s; parameters should be id, queryTemplate(string), createTableTemplate(string)", self.name))
	}
	queryTemplate, ok := args[0].(string)
	if !ok {
		panic(fmt.Errorf("%s; args[0] queryTemplate should be string", self.name))
	}
	if !strings.Contains(queryTemplate, g_tablename_date_mask) {
		panic(fmt.Errorf("%s; AddQueryDef fail, queryId %d, queryTemplate should contain %s to mark date", self.name, id, g_tablename_date_mask))
	}

	createTableTemplate, ok := args[1].(string)
	if !ok {
		panic(fmt.Errorf("args[1] createTableTemplate should be string"))
	}
	if !strings.Contains(createTableTemplate, g_tablename_date_mask) {
		panic(fmt.Errorf("%s; AddQueryDef fail, queryId %d, createTableTemplate should contain %s to mark date", self.name, id, g_tablename_date_mask))
	}

	self.mutex.Lock()
	defer self.mutex.Unlock()

	_, existed := self.queries[id]
	if existed {
		panic(fmt.Errorf("%s; query id %d has already been set", self.name, id))
	}

	self.queries[id] = &ShardingQuery{
		queryTemplate:       queryTemplate,
		createTableTemplate: createTableTemplate,
	}
}

func (self *ShardingByDate) Start() bool {
	self.mutex.Lock()
	defer self.mutex.Unlock()

	// here setup all tables, and guarantee all queries' queryCurrent is available
	self.updateNextShardingDate()
	return self.switchShardingTables(false)
}

func (self *ShardingByDate) queryCore(id int32, nowNS int64) (*ShardingQuery, string, bool) {
	self.mutex.RLock()
	defer self.mutex.RUnlock()

	if !self.dbTablesAllOk {
		return nil, "", false
	}

	query, ok := self.queries[id]
	if !ok {
		log.Error(self.name, "query %d does not exist")
		return nil, "", false
	}

	if nowNS >= self.nextShardingDate {
		return query, "", false
	}

	return query, query.queryCurrent, true
}

func (self *ShardingByDate) GetQuerySqlStr(id int32, nowNS int64) (string, bool) {
	query, str, ok := self.queryCore(id, nowNS)
	if ok {
		return str, true
	}

	if query == nil {
		return "", false
	}

	self.mutex.Lock()
	defer self.mutex.Unlock()

	if !self.dbTablesAllOk {
		return "", false
	}

	oldNextShardingDate := self.nextShardingDate
	self.updateNextShardingDate()
	if oldNextShardingDate == self.nextShardingDate {
		/*
			if two Query call at the same time, and thire queryCore return (query, "", false) at the same time before go down to Lock,
			then updateNextShardingDate maybe do multiple times,
			but only the first update change the data, so next calls need quick return
			query.queryCurrent is updated by the first call, so direct return is ok
		*/
		return query.queryCurrent, true
	}

	ok = self.switchShardingTables(true)
	if !ok {
		return "", false
	}

	return query.queryCurrent, ok
}
func (self *ShardingByDate) OnQueryOk(cnt int32) {
}
